package org.flexharmony.harmonizer.system;

import static java.lang.String.*;
import static org.flexharmony.harmonizer.utils.CommonMethods.*;
import static org.testng.Assert.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.List;

import org.flexharmony.harmonizer.Harmonizer;
import org.flexharmony.harmonizer.config.HarmonyConfiguration;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Parameters;

import uk.co.badgersinfoil.metaas.ActionScriptFactory;
import uk.co.badgersinfoil.metaas.ActionScriptParser;
import uk.co.badgersinfoil.metaas.ActionScriptProject;
import uk.co.badgersinfoil.metaas.dom.ASArg;
import uk.co.badgersinfoil.metaas.dom.ASBooleanLiteral;
import uk.co.badgersinfoil.metaas.dom.ASClassType;
import uk.co.badgersinfoil.metaas.dom.ASCompilationUnit;
import uk.co.badgersinfoil.metaas.dom.ASField;
import uk.co.badgersinfoil.metaas.dom.ASIntegerLiteral;
import uk.co.badgersinfoil.metaas.dom.ASMetaTag;
import uk.co.badgersinfoil.metaas.dom.ASMethod;
import uk.co.badgersinfoil.metaas.dom.ASPackage;
import uk.co.badgersinfoil.metaas.dom.ASStringLiteral;
import uk.co.badgersinfoil.metaas.dom.Expression;
import uk.co.badgersinfoil.metaas.dom.Visibility;
import uk.co.badgersinfoil.metaas.dom.ASMethod.AccessorRole;

/**
 * Base test class for doing end to end system tests. This class takes a Java
 * model class in its constructor and provides some convenience methods for extending
 * test classes to create the expected {@link ASClassType} that should result from
 * running the given Java model class through the harmonizer (see {@link #runHarmonizeTest()}.
 * 
 * Methods are also available for creating an initial {@link ASClassType} that will
 * create an ActionScript class file before the harmonizer is run to simulate incremental
 * updates to ActionScript files.
 * 
 * 
 * @author Corey Baswell
 */
public class ClassComparer
{
  public String classQName;
  
  public ASCompilationUnit initialCU;
  
  public ASPackage initialPackage;
  
  public ASClassType initialClass;
  
  public ASCompilationUnit expectedFinalCU;
  
  public ASPackage expectedPackage;
  
  public ASClassType expectedClass;
  
  public boolean expectChanges;
  
  public HarmonyConfiguration harmonyConfig;
  
  private File flexSrcDir;
  
  private File javaClassesDir;
  
  private ActionScriptFactory asFactory = new ActionScriptFactory();

  public ClassComparer()
  {}
  
  public void setFlexSrcDir(String flexSrcDirName)
  {
    flexSrcDir = new File(flexSrcDirName);
  }
  
  public void setJavaClassDir(String javaClassesDirName)
  {
    javaClassesDir = new File(javaClassesDirName);
  }
  
  @SuppressWarnings("unchecked")
  public void loadClassDefinition(Class javaClassDef)
  {
    classQName = javaClassDef.getCanonicalName();
    expectedFinalCU = asFactory.newClass(classQName);
    expectedPackage = expectedFinalCU.getPackage();
    expectedClass = (ASClassType)expectedFinalCU.getType();
    
    int index = classQName.lastIndexOf('.');
    if (index != -1)
    {
      String packageName = classQName.substring(0, index);
      expectedPackage.setName(packageName);
    }
    
    ASMetaTag metaTag = expectedClass.newMetaTag("RemoteClass");
    metaTag.addParam("alias", classQName);

    clearTestRun();
  }
  
  public void clearTestRun()
  {
    harmonyConfig = new HarmonyConfiguration();
    harmonyConfig.setFlexSrcDir(flexSrcDir);
    harmonyConfig.setAdditiveOnly(true);

    expectChanges = true;
    initialCU = null;
    initialClass = null;
    initialPackage = null;
  }

  @SuppressWarnings("unchecked")
  public void runHarmonizeTest()
  {
    try
    {
      Harmonizer harmonizer = new Harmonizer(harmonyConfig);
      String javaClassFilePath = classQName.replace('.', File.separatorChar) + ".class";
      File javaClassFile = new File(javaClassesDir, javaClassFilePath);
      
      if (!javaClassFile.isFile())
      {
        fail(format("Class %s has no matching class file %s", classQName, javaClassFile.getAbsolutePath()));
      }
  
      File asFile = new File(flexSrcDir, (classQName.replace('.', File.separatorChar) + ".as"));
      if (asFile.exists())
      {
        assertTrue(asFile.delete(), "Unable to delete existing ActionScript file " + asFile.getAbsolutePath() + " before test run.");
      }
      
      /*
       * If there is an initial compilation unit created then create the ActionScript file based on
       * that first before running the harmonizer logic.
       */
      if (initialCU != null)
      {
        ActionScriptProject initProject = asFactory.newEmptyASProject(flexSrcDir.getAbsolutePath());
        initProject.addCompilationUnit(initialCU);
        initProject.writeAll();
      }
      
      /*
       * Run the harmonizer and verify that the ActionScript file has been created
       * and changes resulted as expected.
       */
      
      boolean harmonizeUpdated = harmonizer.harmonize(javaClassFile);
      boolean harmonizeWritten = harmonizer.writeUpdates();

      assertEquals(harmonizeUpdated, expectChanges, 
          format("Class %s had unexpected %s after harmonize.", classQName, (expectChanges ? "no updates" : "updates")));

      assertEquals(harmonizeWritten, expectChanges, 
          format("Harmonizer thinks %s have been made", (expectChanges ? "no changes" : "changes")));
      
      assertTrue(asFile.isFile(), 
          format("ActionScript source file %s does not exists after harmonize.", asFile.getAbsolutePath()));

      /*
       * Go through and verify that the generated ActionScript file matches the expected
       * description that this test has setup.
       */

      FileInputStream in = new FileInputStream(asFile);
      InputStreamReader reader = new InputStreamReader(in);
      ActionScriptParser parser = asFactory.newParser();
      
      ASCompilationUnit actualCU = parser.parse(reader);
      
      reader.close();
      asFile.deleteOnExit();
      
      assertEquals(actualCU.getPackageName(), expectedFinalCU.getPackageName(), 
          format("Class %s expected package %s actual package %s", classQName, 
          expectedFinalCU.getPackageName(), actualCU.getPackageName()));
      
      List<String> actualImports = actualCU.getPackage().findImports();
      List<String> expectedImports = expectedFinalCU.getPackage().findImports();
      
      for (String actualImport : actualImports)
      {
        if (!expectedImports.contains(actualImport))
        {
          fail(format("Class %s has extra import %s", classQName, actualImport));
        }
      }
      
      for (String expectedImport : expectedImports)
      {
        if (!actualImports.contains(expectedImport))
        {
          fail(format("Class %s missing import %s", classQName, expectedImport));
        }
      }
      
      ASClassType actualClass = (ASClassType)actualCU.getType();
      ASClassType expectedClass = (ASClassType)expectedFinalCU.getType();
      
      assertEquals(actualClass.getName(), expectedClass.getName(), 
          format("Class %s expected class name %s but got actual class name %s",
              classQName, expectedClass.getName(), actualClass.getName()));
      
      assertEquals(actualClass.getVisibility(), expectedClass.getVisibility(), 
          format("Class %s expected dynamic %s but got actual dynamic %s", classQName,
          toString(expectedClass.getVisibility()), toString(actualClass.getVisibility())));
      
      assertEquals(actualClass.isDynamic(), expectedClass.isDynamic(), 
          format("Class %s expected dynamic %s but got actual dynamic %s", classQName,
          expectedClass.isDynamic(), actualClass.isDynamic()));
      
      assertEquals(actualClass.isFinal(), expectedClass.isFinal(), 
          format("Class %s expected final %s but got actual final %s", classQName,
          expectedClass.isFinal(), actualClass.isFinal()));
      
      assertEquals(actualClass.getSuperclass(), expectedClass.getSuperclass(),
          format("Class %s expected super class %s but got actual super class %s",
              classQName, expectedClass.getSuperclass(), actualClass.getSuperclass()));
  
      List<ASMetaTag> actualMetaTags = actualClass.getAllMetaTags();
      for (ASMetaTag actualMetaTag : actualMetaTags)
      {
        if (!foundMatchingMetaTag(actualMetaTag, expectedClass.getMetaTagsWithName(actualMetaTag.getName())))
        {
          fail(format("Class %s has extra meta tag %s", classQName, actualMetaTag.getName()));
        }
      }
  
      List<ASMetaTag> expectedMetaTags = expectedClass.getAllMetaTags();
      for (ASMetaTag expectedMetaTag : expectedMetaTags)
      {
        if (!foundMatchingMetaTag(expectedMetaTag, actualClass.getMetaTagsWithName(expectedMetaTag.getName())))
        {
          fail(format("Class %s missing meta tag %s", classQName, expectedMetaTag.getName()));
        }
      }
      
      List<ASField> actualFields = actualClass.getFields();
      for (ASField actualField : actualFields)
      {
        String fieldName = actualField.getName();
        ASField expectedField = expectedClass.getField(fieldName);
        if (expectedField == null)
        {
          fail(format("Class %s has extra field %s", classQName, fieldName));
        }
        
        assertEquals(actualField.getType(), expectedField.getType(), 
            format("Class %s field %s actual type %s expected type %s", 
                classQName, fieldName, actualField.getType(), expectedField.getType()));
  
        assertEquals(actualField.getVisibility(), expectedField.getVisibility(), 
            format("Class %s field %s expected dynamic %s but got actual dynamic %s", 
                classQName, fieldName, toString(expectedField.getVisibility()), toString(actualField.getVisibility())));
  
        assertEquals(actualField.isConst(), expectedField.isConst(), 
            format("Class %s field %s actual constant %s expected constant %s", 
                classQName, fieldName, actualField.isConst(), expectedField.isConst()));
  
        assertEquals(actualField.isStatic(), expectedField.isStatic(), 
            format("Class %s field %s actual static %s expected static %s", 
                classQName, fieldName, actualField.isStatic(), expectedField.isStatic()));
        
        Expression actualExpression = actualField.getInitializer();
        Expression expectedExpression = expectedField.getInitializer();
        
        if ((actualExpression != null) || (expectedExpression != null))
        {
          if ((actualExpression == null) && (expectedExpression != null))
          {
            fail(format("Class %s field %s actual initializer is null but expected initializer class is %s",
                classQName, fieldName, expectedExpression.getClass().toString()));
          }
          else if ((expectedExpression == null) && (expectedExpression != null))
          {
            fail(format("Class %s field %s expected initializer is null but actual initializer class is %s",
                classQName, fieldName, actualExpression.getClass().toString()));
          }
          else if (expectedExpression.getClass() != actualExpression.getClass())
          {
            fail(format("Class %s field %s expected initializer class %s does not match actual initializer class %s", 
                classQName, fieldName, expectedExpression.getClass().toString(), actualExpression.getClass().toString()));
          }
          else if (actualExpression instanceof ASStringLiteral)
          {
            String actualStringLiteral = ((ASStringLiteral)actualExpression).getValue();
            String expectedStringLiteral = ((ASStringLiteral)expectedExpression).getValue();
            assertEquals(actualStringLiteral, expectedStringLiteral, 
                format("Class %s field %s expected initializer value %s actual initializer value %s", 
                    classQName, fieldName, expectedStringLiteral, actualStringLiteral));
          }
          else if (actualExpression instanceof ASIntegerLiteral)
          {
            int actualIntLiteral = ((ASIntegerLiteral)actualExpression).getValue();
            int expectedIntLiteral = ((ASIntegerLiteral)expectedExpression).getValue();
            assertEquals(actualIntLiteral, expectedIntLiteral, 
                format("Class %s field %s expected initializer value %s actual initializer value %s", 
                    classQName, fieldName, expectedIntLiteral, actualIntLiteral));
          }
          else if (actualExpression instanceof ASBooleanLiteral)
          {
            boolean actualBooleanLiteral = ((ASBooleanLiteral)actualExpression).getValue();
            boolean expectedBooleanLiteral = ((ASBooleanLiteral)expectedExpression).getValue();
            assertEquals(actualBooleanLiteral, expectedBooleanLiteral, 
                format("Class %s field %s expected initializer value %s actual initializer value %s", 
                    classQName, fieldName, expectedBooleanLiteral, actualBooleanLiteral));
          }
        }
      }
      
      List<ASField> expectedFields = expectedClass.getFields();
      for (ASField expectedField : expectedFields)
      {
        if (actualClass.getField(expectedField.getName()) == null)
        {
          fail(format("Class %s does not have expected field %s", classQName, expectedField.getName()));
        }
      }
      
      List<ASMethod> actualMethods = actualClass.getMethods();
      List<ASMethod> expectedMethods = expectedClass.getMethods();
      
      for (ASMethod actualMethod : actualMethods)
      {
        ASMethod expectedMethod = findMatchingMethod(actualMethod, expectedMethods);
        if (expectedMethod == null)
        {
          fail(format("Class %s method %s was found but not expected.", classQName, toString(actualMethod)));
        }
      }
      
      for (ASMethod expectedMethod : expectedMethods)
      {
        ASMethod actualMethod = findMatchingMethod(expectedMethod, actualMethods);
        if (actualMethod == null)
        {
          fail(format("Class %s method %s was expected but not found.", classQName, toString(expectedMethod)));
        }
      }
    }
    catch (RuntimeException rexc)
    {
      throw rexc;
    }
    catch (Exception exc)
    {
      throw new RuntimeException(exc);
    }
  }
  
  /**
   * Called from test methods requiring and initial ActionScript class to be
   * established before the harmonizer test run to test incremental updates.
   */
  public void initialCURequired()
  {
    initialCU = asFactory.newClass(classQName);
    initialPackage = initialCU.getPackage();
    initialClass = (ASClassType)initialCU.getType();

    int index = classQName.lastIndexOf('.');
    if (index != -1)
    {
      String packageName = classQName.substring(0, index);
      initialPackage.setName(packageName);
    }
    
    ASMetaTag metaTag = initialClass.newMetaTag("RemoteClass");
    metaTag.addParam("alias", classQName);
  }

  public void addInitialProperty(String propertyName, String type)
  {
    addProperty(propertyName, type, initialClass);
  }
  
  public void addExpectedProperty(String propertyName, String type)
  {
    addProperty(propertyName, type, expectedClass);
  }
  
  public void addProperty(String propertyName,String type, ASClassType asClass)
  {
    String fieldName = "_" + propertyName;

    asClass.newField(fieldName, Visibility.PRIVATE, type);

    ASMethod getter = asClass.newMethod(propertyName, Visibility.PUBLIC, type);
    getter.setAccessorRole(AccessorRole.GETTER);
    getter.addStmt("return " + fieldName + ";");

    ASMethod setter = asClass.newMethod(propertyName, Visibility.PUBLIC, "void");
    setter.setAccessorRole(AccessorRole.SETTER);
    setter.addParam("value", type);
    setter.addStmt(fieldName + " = value;");
  }
  
  @SuppressWarnings("unchecked")
  private boolean foundMatchingMetaTag(ASMetaTag metaTag, List<ASMetaTag> compareMetaTags)
  {
    List params = metaTag.getParams();
    
    for (ASMetaTag compareMetaTag : compareMetaTags)
    {
      List compareParams = compareMetaTag.getParams();

      if (compareParams.size() == params.size())
      {
        boolean allParamsFound = true;
        for (Object param : params)
        {
          boolean paramMatchFound = false;
          
          for (Object compareParam : compareParams)
          {
            if ((param instanceof ASMetaTag.Param) && (compareParam instanceof ASMetaTag.Param))
            {
              ASMetaTag.Param metaParam = (ASMetaTag.Param)param;
              ASMetaTag.Param compareMetaParam = (ASMetaTag.Param)param;
              
              if (compareMetaParam.getName().equals(metaParam.getName()) 
                  && compareMetaParam.getValue().equals(metaParam.getValue()))
              {
                paramMatchFound = true;
                break;
              }
              else if (compareParam.equals(param))
              {
                paramMatchFound = true;
                break;
              }
            }
          }
          
          if (!paramMatchFound)
          {
            allParamsFound = false;
            break;
          }
        }
        if (allParamsFound)
        {
          return true;
        }
      }
    }
    
    return false;
  }
  
  @SuppressWarnings("unchecked")
  private ASMethod findMatchingMethod(ASMethod method, List<ASMethod> compareMethods)
  {
    List<ASArg> args = method.getArgs();
    for (ASMethod compareMethod : compareMethods)
    {
      if (compareMethod.getName().equals(method.getName())
          && (compareMethod.getVisibility() == method.getVisibility())
          && (compareMethod.isStatic() == method.isStatic())
          && (compareMethod.getAccessorRole() == method.getAccessorRole())
          && nullEquals(compareMethod.getType(), method.getType()))
      {
        List<ASArg> compareArgs = compareMethod.getArgs();
        if (args.size() == compareArgs.size())
        {
          boolean allArgsMatched = true;
          for (int i = 0; i < args.size(); i++)
          {
            ASArg arg = args.get(i);
            ASArg compareArg = compareArgs.get(i);
            
            if (!arg.getName().equals(compareArg.getName())
                || !arg.getType().equals(compareArg.getType())
                || !nullEquals(arg.getDefaultString(), compareArg.getDefaultString()))
            {
              allArgsMatched = false;
              break;
            }
          }
          
          if (allArgsMatched)
          {
            return compareMethod;
          }
        }
      }
    }
    return null;
  }

  
  private String toString(Visibility visibility)
  {
    if (visibility == Visibility.DEFAULT)
    {
      return "default";
    }
    else if (visibility == Visibility.INTERNAL)
    {
      return "internal";
    }
    else if (visibility == Visibility.PRIVATE)
    {
      return "private";
    }
    else if (visibility == Visibility.PROTECTED)
    {
      return "protected";
    }
    else if (visibility == Visibility.PUBLIC)
    {
      return "public";
    }
    else
    {
      return "unknown";
    }
  }
  
  @SuppressWarnings("unchecked")
  private String toString(ASMethod method)
  {
    String methodStr = toString(method.getVisibility());
    if (method.isStatic())
    {
      methodStr += " static";
    }
    
    methodStr += " function";
    if (method.getAccessorRole() == AccessorRole.GETTER)
    {
      methodStr += " get";
    }
    else if (method.getAccessorRole() == AccessorRole.SETTER)
    {
      methodStr += " set";
    }
    
    methodStr += " " + method.getName() + "(";
    
    List<ASArg> args = method.getArgs();
    for (int i = 0; i < args.size(); i++)
    {
      if (i != 0) methodStr += ", ";
      ASArg arg = args.get(i);
      methodStr += arg.getName() + ":" + arg.getType();
    }
    
    methodStr += "):";
    
    if (method.getType() == null)
    {
      methodStr += "*";
    }
    else
    {
      methodStr += method.getType();
    }
    
    return methodStr;
  }
}
